﻿using CodeSHARPer.Services;
using ICSharpCode.CodeCompletion;
using Microsoft.CSharp;
using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;

namespace CodeSHARPer.AvalonEdit.Intellisense
{
    /// <summary>
    /// This is a simple script provider that adds a few using statements to the C# scripts (.csx files)
    /// </summary>
    class ScriptProvider : ICSharpScriptProvider
    {
        private AppDomain _currentDomain;
        public string GetUsing()
        {
            return BuilldNamespaces(prettyPrint: true);
        }

        public string GetVars()
        {
            return null;
        }

        public string[] GetAssemblies()
        {
            return options.AssembliesNames.ToArray();
        }

        public string GetNamespacesUsed()
        {
            return BuilldNamespaces(prettyPrint: true);
        }

        private string BuilldNamespaces(bool prettyPrint)
        {
            StringBuilder usings = new StringBuilder();
            if (options != null)
            {
                foreach (string ns in options.Usings)
                {
                    usings.AppendFormat(UsingFormatTemplate, ns, prettyPrint ? Environment.NewLine : " ");
                }
            }

            return usings.ToString();
        }

        const string netPipeAddress = "net.pipe://localhost/CodeSharperConsoleRedirector";
        const string UsingFormatTemplate = "using {0};{1}"; //{0} using {1} newLine
        const string ClassStartTemplate = @"public class CodeSHARPer {{ 
                [STAThread] 
                public static {0} 
            }}

            internal class CodeSHARPer_Console_Redirector {{
                static ICustomConsole _proxy = null;
                internal static ICustomConsole CreateChannelProxy()
                {{
                    if (_proxy != null)
                        return _proxy;
                    string endPointAddr = ""{1}"";
                    System.ServiceModel.NetNamedPipeBinding  namedPipeBinding = new System.ServiceModel.NetNamedPipeBinding ();
                    namedPipeBinding.TransactionFlow = false;
                    namedPipeBinding.Security.Transport.ProtectionLevel =
                      System.Net.Security.ProtectionLevel.EncryptAndSign;
                    System.ServiceModel.EndpointAddress endpointAddress =
                       new System.ServiceModel.EndpointAddress(endPointAddr);

                    _proxy =
                      System.ServiceModel.ChannelFactory<ICustomConsole>.CreateChannel(
                      namedPipeBinding, endpointAddress);
                    return _proxy;
                }}

                internal static void WriteLine(string str)
                {{
                    var proxy = CreateChannelProxy();
                    proxy.ConsoleWriteLine(str);
                }}

                internal static void Write(string str)
                {{
                    var proxy = CreateChannelProxy();
                    proxy.ConsoleWrite(str);
                }}
        }}

        [System.ServiceModel.ServiceContract]
        public interface ICustomConsole
        {{
            [System.ServiceModel.OperationContract]
            void ConsoleWriteLine(string str);

            [System.ServiceModel.OperationContract]
            void ConsoleWrite(string str);
        }}

"; //{0} code content
        const string MainTemplate = "public class CodeSHARPer {{ [STAThread] public static void Main() {{ {1}{0} }}}}"; //{0} newLine {1} code content
        const string FormatCodeTemplate = "public class CodeSHARPer{1}{{{1}void CS(){1}{{{1}{0}{1}}}{1}}}";

        CompilerOptions options;
        public CompilerOptions Options
        {
            get { return options; }
            private set { options = value; }
        }

        int offSet;
        public int OffSet
        {
            get { return offSet; }
            private set { offSet = value; }
        }

        string fileName;
        public ScriptProvider(string fileName)
        {
            this.fileName = fileName;
            ReadOptionsFile(this.fileName);
        }

        /// <summary>
        /// Open the file and reads the json options.
        /// </summary>
        /// <param name="file"></param>
        public void ReadOptionsFile(string file)
        {
            if (File.Exists(fileName))
            {
                using (StreamReader sr = new StreamReader(fileName))
                {
                    //read the options for this file
                    if (!sr.EndOfStream)
                        options = JsonSerializer.Deserialize<CompilerOptions>(sr.ReadLine());
                    else //load default options
                        options = JsonSerializer.Deserialize<CompilerOptions>(Properties.Resources.DefaultOptionsv40);
                }
            }
        }

        /// <summary>
        /// Generate code ready to compile, adding usings, class name and main method.
        /// </summary>
        /// <param name="code">script content with no main method, this is the code that on screen.</param>
        /// <returns></returns>
        public string GenerateCodeFromCSX(string code)
        {
            if (options.CodeType == CodeScriptType.Program)
                return GenerateCodeFromCS(code);

            StringBuilder builder = new StringBuilder();
            builder.Append(BuilldNamespaces(prettyPrint: false));

            builder.AppendFormat(MainTemplate, Environment.NewLine, code);

            OffSet = builder.Length - code.Length - 5;

            return builder.ToString();
        }

        /// <summary>
        /// Generate code ready to compile, adding usings, class name and main method.
        /// </summary>
        /// <param name="code">script content with a main method, this is the code that on screen.</param>
        /// <returns></returns>
        private string GenerateCodeFromCS(string code)
        {
            StringBuilder builder = new StringBuilder();
            builder.Append(BuilldNamespaces(prettyPrint: false));
            code = code.Replace("Console.Write", "CodeSHARPer_Console_Redirector.Write");
            builder.AppendFormat(ClassStartTemplate, code, netPipeAddress);
            OffSet = builder.Length - code.Length - 2;

            return builder.ToString();
        }

        public string GenerateCodeForFormating(string code)
        {
            return string.Format(FormatCodeTemplate, code, Environment.NewLine);
        }

        public string ReadFromFormatedCode(string formatedCode)
        {
            StringBuilder sb = new StringBuilder();
            foreach (string line in formatedCode.Substring(45, formatedCode.Length - 53).Split(new string[] { Environment.NewLine }, StringSplitOptions.None))
            {
                if (line.StartsWith("\t\t"))
                    sb.AppendLine(line.Substring(2, line.Length - 2));
                else
                    sb.AppendLine(line);
            }

            return sb.ToString();
        }

        public CompilerResults Compile(string code)
        {
            if (_currentDomain != null)
                AppDomain.Unload(_currentDomain);
            string baseDir = AppDomain.CurrentDomain.BaseDirectory;

            CodeDomProvider compiler = new CSharpCodeProvider(options.CodeDomProviderOptions);
            CompilerParameters parameters = new CompilerParameters();
            parameters.WarningLevel = 4;
            parameters.GenerateExecutable = true;
            parameters.GenerateInMemory = false;
            parameters.MainClass = "CodeSHARPer";
            parameters.OutputAssembly = System.IO.Path.GetTempPath() + "CodeSharper\\temp"+Guid.NewGuid().ToString()+".dll";
            if (options != null)
            {
                //parameters.ReferencedAssemblies.Add(this.GetType().Assembly.Location);
                parameters.ReferencedAssemblies.Add(typeof(System.ServiceModel.BasicHttpBinding).Assembly.Location);
                foreach (string assembly in options.AssembliesNames)
                {
                    string fullname = "";
                    if (!File.Exists(assembly))
                    {
                        // the assembly is not a path to the file, so we try to load the assembly and get its location
                        //the assembly must be in fullname, like System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
                        Assembly a = null;
                        try
                        {
                            a = Assembly.Load(assembly);
                        }
                        catch (Exception) { }

                        if (a != null)
                            fullname = a.Location;
                    }
                    else
                        fullname = assembly;

                    parameters.ReferencedAssemblies.Add(fullname);
                }
            }
            var results = compiler.CompileAssemblyFromSource(parameters, this.GenerateCodeFromCSX(code));
            
            return results;
        }

        public void RunCode(CompilerResults results)
        {
            _currentDomain = AppDomain.CreateDomain("NewDomain", null, System.IO.Path.GetTempPath() + "CodeSharper", System.IO.Path.GetTempPath() + "CodeSharper", AppDomain.CurrentDomain.ShadowCopyFiles);
            _currentDomain.ExecuteAssemblyByName(results.CompiledAssembly.FullName);
        }

        /// <summary>
        /// Saves the code with the options in the location
        /// </summary>
        public void Save(string location, string code)
        {
            string json = JsonSerializer.Serialize(options);

            File.WriteAllText(location, string.Format("{0}{2}{1}", json, code, Environment.NewLine));
        }

        public void AlterUsings(string usings)
        {
            if (usings.EndsWith(";"))
                usings += Environment.NewLine;

            string[] usingsList = usings.Split(new string[] { ";" + Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
            if (usingsList != null && usingsList.Length > 0)
            {
                if (this.options.Usings == null)
                    this.options.Usings = new List<string>();

                this.options.Usings.Clear();
                foreach (string usingName in usingsList)
                {
                    this.options.Usings.Add(usingName.Remove(0, "using".Length).Trim());
                }
            }
        }

        public void AlterReferences(string[] references)
        {
            if (this.options.AssembliesNames == null)
                this.options.AssembliesNames = new List<string>(references);
            else
            {
                this.options.AssembliesNames.Clear();
                this.options.AssembliesNames.AddRange(references);
            }
        }

        public void SaveOptions()
        {
            System.Web.Script.Serialization.JavaScriptSerializer serializer = new System.Web.Script.Serialization.JavaScriptSerializer();
            string json = serializer.Serialize(options);
        }
    }
}
